/*
 * Copyright 2005 Lumis EIP Tecnologia da Informa��o
 */

package lumis.util.security.acl;

import lumis.portal.*;
import lumis.portal.authentication.SessionConfig;
import lumis.portal.cache.PortalCache;
import lumis.portal.dao.DaoException;
import lumis.portal.dao.DaoFactory;
import lumis.portal.manager.ManagerException;
import lumis.portal.serialization.DeserializationConfig;
import lumis.portal.serialization.ISerializationManager2;
import lumis.portal.transaction.AbstractTransactionObserver;
import lumis.portal.transaction.ITransactionObserver;
import lumis.portal.transaction.PortalTransactionFactory;
import lumis.portal.transaction.TransactionRequiredException;
import lumis.portal.user.UserConfig;
import lumis.util.*;
import lumis.util.log.ILogger;
import lumis.util.log.LoggerFactory;

import java.io.OutputStream;
import java.util.Collection;
import java.util.HashMap;

import org.w3c.dom.Node;

/**
 * Base AclManager implementation. This is an abstract implementation,
 * that contains the logic that all acls should have. Specific acl
 * managers should extend this class.
 * 
 * @since 4.0.0
 * @version $Revision: 11804 $ $Date: 2010-07-29 10:06:21 -0300 (Thu, 29 Jul 2010) $
 */
public abstract class AclManager implements IAclManager
{
	private static ILogger logger = LoggerFactory.getLogger(AclManager.class);

	protected static PortalCache<AccessControlList> aclCache = new PortalCache<AccessControlList>("lumis.util.security.acl.AccessControlList");
	
	/**
	 * Transaction observer that cleans up any orphan ACL Entry or workflow
	 * permission from the Lumis Portal database, after the transaction commit.
	 * @since 5.6.0
	 */
	private static ITransactionObserver orphanACLEntriesCleaner = new AbstractTransactionObserver()
	{
		@Override
		public void afterCommit() throws PortalException
		{
			ITransaction transaction = PortalTransactionFactory.createTransaction();
			try
			{
				transaction.begin();
				
				// delete inherited ACL entries with parents removed
				DaoFactory.getAccessControlDao().deleteInheritingEntriesWithoutParent(transaction);
				
				// delete workflow permissions without corresponding ACL entry
				try
				{
					Object workflowManager = Class.forName("lumis.content.ContentManagerFactory").getMethod("getWorkflowManager", (Class[])null).invoke(null, (Object[])null);
					workflowManager.getClass().getMethod("removeWorkflowPermissionsWithoutAcl", (Class[])null).invoke(workflowManager, (Object[])null);
				}
				catch (Exception e)
				{
					throw new UnexpectedException(e);
				}
				
				transaction.commit();
			}
			finally
			{
				transaction.dispose();
			}
		}
	};
	
	public abstract AccessControlList get(SessionConfig sessionConfig, String id, ITransaction transaction) throws ManagerException, PortalException;

	protected abstract AccessControlList getAclInternal(SessionConfig sessionConfig, String itemId, ITransaction transaction) throws ManagerException, PortalException;

	protected abstract int getRequiredPermissions() throws PortalException;

	/**
	 * Checks if the required permissions were given for someone. If not an
	 * exception is thrown.
	 * 
	 * @param acl
	 *            the acl to be checked.
	 * @throws PortalException
	 *             if the required permissions were not given.
	 */
	protected void checkRequiredPermissionsInternal(AccessControlList acl) throws PortalException
	{
		// during deserialization, assume that a ACL that is inheriting has required permissions
		// TODO: this is not perfect, since a deny may cause an unaccessible element. this needs to be reviewed later.   
		try
		{
			if(acl.isInheriting() && PortalTransactionFactory.getCurrentTransaction().getAttribute(ISerializationManager2.ATTRIBUTE_DESERIALIZING) != null)
				return;
		}
		catch(TransactionRequiredException e)
		{
			// if not in a transaction, it is not inside deserialization
		}

		int requiredPermissions = getRequiredPermissions();

		// for each acl entry, clear allowed permissions from
		// requiredPermissions variable
		for (AccessControlEntry entry : acl.getEntries())
		{
			requiredPermissions &= ~entry.getPermissions().getAllowPermissions();
			if (requiredPermissions == 0)
				break;
		}
		
		// Make sure that atleast one of the acl entries has the 0 position permission which 
		// is the Manage Security permission
		boolean atleastOneSecurityPermission = false;
		for (AccessControlEntry entry : acl.getEntries())
		{
			if(entry.getPermissions().checkPermissionByPosition(0))
			{
				atleastOneSecurityPermission = true;
				break;
			}
		}
		
		if(!atleastOneSecurityPermission)
			throw new PortalException("STR_ATLEAST_ONE_PRINCIPAL_MUST_HAVE_SECURITY_PERMISSION");		

		// if there is still a required permission, then it was not allowed in a
		// entry, so throw an exception
		if (requiredPermissions > 0)
			throw new PortalException("STR_REQUIRED_PERMISSION_NOT_APPLIED");
	}

	protected String add(AccessControlList parentAcl, HashMap<Integer, Integer> permissionsMap, int[] implies, ITransaction transaction) throws PortalException
	{
		// create new acl
		String aclId = PortalUtil.generateNewGuid();
		AccessControlList acl = new AccessControlList(aclId, null, true);

		// inherit permissions from parentAcl
		if (permissionsMap != null)
			acl.inherit(parentAcl, permissionsMap);
		else
			acl.inherit(parentAcl);
		
		add(acl, transaction);
		
		return aclId;
	}
	
	public void add(AccessControlList acl, ITransaction transaction) throws PortalException
	{
		// check if the required permissions were given in the acl
		checkRequiredPermissionsInternal(acl);

		// persist acl
		DaoFactory.getAccessControlDao().add(acl, transaction);
	}

	public boolean exists(String aclId, ITransaction transaction) throws DaoException, PortalException
	{
		return DaoFactory.getAccessControlDao().exists(aclId, transaction);
	}
	
	protected void update(AccessControlList acl, ITransaction transaction) throws ManagerException, PortalException
	{
		// check if the required permissions were given in the acl
		checkRequiredPermissionsInternal(acl);

		// fix inheritance attributes
		cleanupInvalidData(acl);

		// persist acl
		DaoFactory.getAccessControlDao().update(acl, transaction);
		
		// schedule to cleanup any orphan ACL Entry that may exist due to the 
		// change in the ACL
		transaction.addObserver(orphanACLEntriesCleaner);
		
		// clear acl from cache
		removeFromCache(acl.getId(), transaction);
	}

	public void addOrUpdate(AccessControlList acl, ITransaction transaction) throws ManagerException, PortalException
	{
		if (DaoFactory.getAccessControlDao().get(acl.getId(), transaction) != null)
			update(acl, transaction);
		else
			add(acl, transaction);
	}

	protected void cleanupInvalidData(AccessControlList acl) throws PortalException
	{
		if (!acl.isInheriting())
		{
			AccessControlEntry[] aclEntries = acl.getEntries();
			for (AccessControlEntry aclEntry : aclEntries)
			{
				aclEntry.removeInheritance();
			}
		}
	}

	public void delete(String aclId, ITransaction transaction) throws PortalException
	{
		// delete acl from persistence
		DaoFactory.getAccessControlDao().delete(aclId, transaction);

		// clear acl from cache
		removeFromCache(aclId, transaction);
	}

	public void deleteAclEntryByPrincipalId(String principalId, ITransaction transaction) throws PortalException
	{
		Collection<String> aclIds = DaoFactory.getAccessControlDao().getAclIdsByPrincipalId(principalId, transaction);

		for (String aclId : aclIds)
		{
			// clear acl from cache
			removeFromCache(aclId, transaction);
		}

		DaoFactory.getAccessControlDao().deleteAclEntryByPrincipalId(principalId, transaction);
	}

	public void removeFromCache(String aclId, ITransaction transaction) throws DaoException, PortalException
	{
		// remove the acl from cache
		aclCache.remove(aclId, transaction);
		if (logger.isDebugEnabled())
			logger.debug("ACL " + aclId + " removed from cache");

		// remove the acl's children from cache
		Collection<String> childrenIds = DaoFactory.getAccessControlDao().getIdsByParentId(aclId, transaction);
		for (String childId : childrenIds)
		{
			removeFromCache(childId, transaction);
		}
	}
	
	public void clearCache(ITransaction transaction) throws PortalException
	{
		if (transaction != null)
			aclCache.clear(transaction);
		else
			aclCache.clear();
	}

	public boolean checkPermission(SessionConfig sessionConfig, String itemId, IPermission permission, ITransaction transaction) throws ManagerException, PortalException
	{
		String userId = sessionConfig.getUserId();
		return checkPermission(sessionConfig, userId, itemId, permission, transaction);
	}

	/**
	 * For the given principalId, all the groups that the principal is member of
	 * (recursively) are collected and permission is calculated on this
	 * collection.
	 */
	public boolean checkPermission(SessionConfig sessionConfig, String userId, String itemId, IPermission permission, ITransaction transaction) throws ManagerException, PortalException
	{
		// SYSTEM user has permission for everything
		if (UserConfig.USER_SYSTEM_ID.equals(userId))
			return true;

		boolean allowed = false;

		if (userId != null)
		{
			AccessControlList acl = getAclInternal(sessionConfig, itemId, transaction);

			if (acl == null)
				return true;

			allowed = acl.checkPermission(userId, permission);
		}

		return allowed;
	}

	public void serialize(SessionConfig sessionConfig, String securableId, OutputStream outputStream, ITransaction transaction) throws ManagerException, PortalException
	{
		// TODO: validate userSessionId
		AccessControlList accessControlList = get(sessionConfig, securableId, transaction);
		accessControlList.serialize(accessControlList, outputStream);
	}

	public String deserialize(SessionConfig sessionConfig, DeserializationConfig deserializationConfig, Node accessControlListNode, boolean onlyAddSimpleAcls, ITransaction transaction) throws ManagerException, PortalException
	{
		if (onlyAddSimpleAcls)
		{
			AccessControlList accessControlList = null;
			if (deserializationConfig.getCreateNewIds())
			{
				String newId = PortalUtil.generateNewGuid();
				deserializationConfig.setIdMapping(XmlUtil.readAttributeString("id", accessControlListNode), newId);
				accessControlList = new AccessControlList(newId, null, true);
			}
			else
			{
				accessControlList = new AccessControlList(XmlUtil.readAttributeString("id", accessControlListNode), null, true);
			}
			
			AccessControlList accessControlListTemp = null;
			try
			{
				accessControlListTemp = DaoFactory.getAccessControlDao().get(accessControlList.getId(), transaction);
			}
			catch (Exception e)
			{
				accessControlListTemp = null;
			}
			
			// if the access control list does not exist, add it
			if (accessControlListTemp == null)
				DaoFactory.getAccessControlDao().add(accessControlList, transaction);
			
			return accessControlList.getId();					
		}
		else
		{
			String aclId = deserializationConfig.getIdMapping(XmlUtil.readAttributeString("id", accessControlListNode));		
			AccessControlList accessControlList = new AccessControlList(aclId, null, true);
			accessControlList.deserialize(accessControlListNode);
			accessControlList.setParentId(deserializationConfig.getIdMapping(accessControlList.getParentId()));
			
			// replace principal id on the acl entries according to the deserialization mapping
			AccessControlEntry[] entries = accessControlList.getEntries();
			for (int i=0; i<entries.length; i++)
			{
				if (deserializationConfig.containsIdMapping(entries[i].getPrincipalId()))
				{
					entries[i] = new AccessControlEntry(deserializationConfig.getIdMapping(entries[i].getPrincipalId()), 
							entries[i].getPermissions(), entries[i].isInherited());
				}
			}
			accessControlList = new AccessControlList(aclId, entries, accessControlList.isInheriting(), accessControlList.getParentId());
			
			// do the update
			DaoFactory.getAccessControlDao().update(accessControlList, transaction);

			// clear acl from cache
			removeFromCache(accessControlList.getId(), transaction);

			return accessControlList.getId();			
		}
	}
	public boolean keyExists(SessionConfig sessionConfig, Object configKey, Object config, ITransaction transaction)
	{
		try
		{
			return exists((String)configKey, transaction);
		}
		catch (PortalException e)
		{
			throw new IllegalStateException("Could not check a principal key persistence.");
		}
	}

}
